﻿// Tierless
// Copyright © 2010 Alphora
// This file is licensed under a modified BSD-license (see tierless_license.txt).

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;

using Tierless.Infrastructure.Data;
using Tierless.Framework.Data;
using Tierless.Framework.Entities;
using Tierless.Framework.EntityTypes;
using Tierless.Framework.Utilities;
using Tierless.Framework.Server.Data;

namespace Tierless.Framework.Server.Data
{
	/// <summary>
	/// Provides low-level entity access to the entities in the entity store.
	/// </summary>
	public class EntityProvider : IServerEntityTypeProvider
	{
		/// <summary>
		/// Initializes a new instance of the EntityProvider class.
		/// </summary>
		/// <param name="ADatabase">The database to use for entity storage.</param>
		public EntityProvider(Database ADatabase)
		{
			FDatabase = ADatabase;
			FEntityTypeProvider = new EntityTypeProvider(ADatabase);
			// TODO: This will need to be re-designed in order to support multiple databases per application.
			EntityTypeRepository.EntityTypeProvider = FEntityTypeProvider;
		}
		
		private Database FDatabase;
		protected internal Database Database
		{
			get { return FDatabase; }
		}

		private EntityTypeProvider FEntityTypeProvider;
		protected EntityTypeProvider EntityTypeProvider
		{
			get { return FEntityTypeProvider; }
		}

		#region IEntityTypeProvider Members

		/// <inheritdoc/>
		public EntityType GetEntityType(string AEntityTypeName)
		{
			return EntityTypeProvider.GetEntityType(AEntityTypeName);
		}

		/// <inheritdoc/>
        public void EnsureEntityTypes(IEnumerable<string> entityTypeNames)
        {
            // Do nothing for server implementation
        }

		#endregion
		
		#region IServerEntityTypeProvider Members
		
		/// <inheritdoc/>
		public ServerEntityType GetServerEntityType(string AEntityTypeName)
		{
			return EntityTypeProvider.GetServerEntityType(AEntityTypeName);
		}
		
		#endregion

		protected virtual RelatedEntitySpecifier ResolveRelatedEntitySpecifier(EntityReference AReference, bool AForUpdate, EntityTraversal ATraversal, Dictionary<string, string> AVisitedReferences)
		{
			var LRelatedEntityTypeName = 
				AReference.Kind == ReferenceKind.Child 
					? AReference.SourceEntityTypeName 
					: AReference.TargetEntityTypeName;

			return
				new RelatedEntitySpecifier
				(
					ATraversal.TraversalType == EntityTraversalType.Recursive
						? ResolveTraversal(LRelatedEntityTypeName, null, AReference.Kind == ReferenceKind.Child ? AForUpdate : false, ATraversal, AVisitedReferences)
						: new EntitySpecifier(LRelatedEntityTypeName),
					AReference.Kind == ReferenceKind.Child
						? AReference.TargetColumns.AsStringArray
						: AReference.SourceColumns.AsStringArray,
					AReference.Kind == ReferenceKind.Child
						? AReference.SourceColumns.AsStringArray
						: AReference.TargetColumns.AsStringArray
				);
		}
		
		protected virtual EntitySpecifier ResolveTraversal(string AEntityTypeName, string ACommandName, bool AForUpdate, EntityTraversal ATraversal, Dictionary<string, string> AVisitedReferences)
		{
			var LRelatedEntitySpecifiers = new List<RelatedEntitySpecifier>();

			var LEntityType = EntityTypeProvider.GetEntityType(AEntityTypeName);

			if (ATraversal.RelatedEntityTypeNames == null)
			{
				foreach (var LReference in LEntityType.References.Where(AReference => !AVisitedReferences.ContainsKey(AReference.Name)))
				{
					AVisitedReferences.Add(LReference.Name, LReference.Name);
					LRelatedEntitySpecifiers.Add(ResolveRelatedEntitySpecifier(LReference, AForUpdate, ATraversal, AVisitedReferences));
				}
			}
			else
			{
				foreach (var LReference in from LRelatedEntityTypeName in ATraversal.RelatedEntityTypeNames
					from LReference in LEntityType.References
					where (((LReference.Kind == ReferenceKind.Child) && (LReference.SourceEntityTypeName == LRelatedEntityTypeName)) || ((LReference.Kind == ReferenceKind.Lookup) && (LReference.TargetEntityTypeName == LRelatedEntityTypeName))) && !AVisitedReferences.ContainsKey(LReference.Name)
					select LReference)
				{
					AVisitedReferences.Add(LReference.Name, LReference.Name);
					LRelatedEntitySpecifiers.Add(ResolveRelatedEntitySpecifier(LReference, AForUpdate, ATraversal, AVisitedReferences));
				}
			}
			return new EntitySpecifier(AEntityTypeName, ACommandName, LRelatedEntitySpecifiers);
		}

		protected EntitySpecifier ResolveTraversal(string AEntityTypeName, string ACommandName, bool AForUpdate, EntityTraversal ATraversal)
		{
			return ResolveTraversal(AEntityTypeName, ACommandName, AForUpdate, ATraversal, new Dictionary<string, string>());
		}
		
		protected string PrepareSelectStatement(EntityType AEntityType, string AStatement, IList<EntityParameter> AFilter, EntityOrder AOrder, bool AForUpdate)
		{
			AStatement = AStatement.Replace("/*%tablehint%*/", AForUpdate ? "with (updlock)" : "");

			var LFilterString = new StringBuilder();
			if ((AFilter != null) && (AFilter.Count > 0))
			{
				foreach (var LFilterColumn in AFilter)
				{
					if (LFilterString.Length > 0)
						LFilterString.Append(" and ");

					LFilterString.AppendFormat("(T.{0} = @{0})", LFilterColumn.Name);
				}
			}

			AStatement = AStatement.Replace("/*%filtercondition%*/", LFilterString.Length > 0 ? LFilterString.ToString() : "1 = 1");

			if (AOrder != null)
			{
				var LOrderString = new StringBuilder();
				foreach (var LOrderColumn in AOrder.Columns)
				{
					if (LOrderString.Length > 0)
						LOrderString.Append(", ");
					LOrderString.AppendFormat("{0} {1}", LOrderColumn.Name, LOrderColumn.Direction == OrderDirection.Ascending ? "asc" : "desc");
				}

				AStatement = String.Format("{0} order by {1}", AStatement, LOrderString);
			}

			return AStatement;
		}

		protected virtual ServerEntityCommand ResolveCommandName(ServerEntityType AServerEntityType, string ACommandName)
		{
			if (String.IsNullOrEmpty(ACommandName))
				return AServerEntityType.SelectCommand;

			if (AServerEntityType.HasCommandDef(ACommandName))
				return AServerEntityType.GetCommandDef(ACommandName);

			throw new ArgumentException(String.Format("Could not resolve command name '{0}' for entity type '{1}'.", ACommandName, AServerEntityType.Name));
		}

		/// <summary>
		/// Returns an entity set containing the entities satisfying the given entity query.
		/// </summary>
		/// <param name="AEntityQuery">The query describing the entities to be retrieved.</param>
		/// <returns>An entity set containing the entities satisfying the given entity query.</returns>
		public EntitySet Get(EntityQuery AEntityQuery)
		{
			var LServerEntityType = EntityTypeProvider.GetServerEntityType(AEntityQuery.EntitySpecifier.EntityTypeName);
			var LEntityType = EntityTypeProvider.GetEntityType(AEntityQuery.EntitySpecifier.EntityTypeName);
			EntitySet LEntitySet;

		    LServerEntityType.DoBeforeSelect(this, AEntityQuery);

			var LCommandType = ResolveCommandName(LServerEntityType, AEntityQuery.EntitySpecifier.CommandName);

			var LInputParams = new List<Param>();

			if (LEntityType.IsTemporal)
				LInputParams.Add(new Param(LEntityType.FromDateColumnName, AEntityQuery.AsOf));

			var LFilterParams = new List<EntityParameter>();
			foreach (var LFilterParam in AEntityQuery.Filter)
			{
				if ((LFilterParam.EntityName == null) || LFilterParam.EntityName == AEntityQuery.EntitySpecifier.EntityTypeName)
				{
					LFilterParams.Add(LFilterParam);
					LInputParams.Add(new Param(LFilterParam.Name, LFilterParam.Value));
				}
			}

			using
			(
				var LReader =
					LCommandType.CommandType == ServerEntityCommandType.Statement
						?
							Database.Open
							(
								PrepareSelectStatement
								(
									LEntityType,
									LCommandType.Statement,
									LFilterParams,
									(AEntityQuery.Order ?? LEntityType.DefaultOrder),
									AEntityQuery.EntitySpecifier.ForUpdate
								),
								LInputParams.ToArray()
							)
						:
							Database.OpenFromProcedure
							(
								LCommandType.Procedure,
								LInputParams.ToArray()
							)
			)
			{
				LEntitySet = LoadSetFromReader(LServerEntityType, LEntityType, LReader);
			}

			PopulateRelatedEntities(AEntityQuery, LEntityType, LEntitySet);
			
			return LEntitySet;
		}
		
		private static EntityReference ResolveRelatedEntitySpecifier(EntityType AEntityType, RelatedEntitySpecifier ARelatedEntitySpecifier)
		{
			foreach (var LReference in AEntityType.References)
			{
				if 
				(
					(
						(LReference.SourceEntityTypeName == AEntityType.Name)
							&& (LReference.TargetEntityTypeName == ARelatedEntitySpecifier.EntitySpecifier.EntityTypeName)
							&& (LReference.SourceColumnNames.Combine() == ARelatedEntitySpecifier.ColumnNames.Combine())
							&& (LReference.TargetColumnNames.Combine() == ARelatedEntitySpecifier.RelatedColumnNames.Combine())
					)
						||
						(
							(LReference.TargetEntityTypeName == AEntityType.Name)
								&& (LReference.SourceEntityTypeName == ARelatedEntitySpecifier.EntitySpecifier.EntityTypeName)
								&& (LReference.TargetColumnNames.Combine() == ARelatedEntitySpecifier.ColumnNames.Combine())
								&& (LReference.SourceColumnNames.Combine() == ARelatedEntitySpecifier.RelatedColumnNames.Combine())
						)
				)
					return LReference;
			}
			
			throw 
				new ArgumentException
				(
					String.Format
					(
						"Could not resolve related entity specifier '{0}:{1}' from entity specifier '{2}:{3}'.", 
						ARelatedEntitySpecifier.EntitySpecifier.EntityTypeName, 
						ARelatedEntitySpecifier.RelatedColumnNames.Combine(), 
						AEntityType.Name, 
						ARelatedEntitySpecifier.ColumnNames.Combine()
					)
				);
		}

		private void PopulateRelatedEntities(EntityQuery AEntityQuery, EntityType AEntityType, EntitySet AEntitySet)
		{
			foreach (var LEntity in AEntitySet)
			{
				LEntity.BeginPopulate();
				try
				{
					foreach (var LRelatedEntitySpecifier in AEntityQuery.EntitySpecifier.RelatedEntitySpecifiers)
					{
						if (LRelatedEntitySpecifier.Reference == null)
							LRelatedEntitySpecifier.Reference = ResolveRelatedEntitySpecifier(AEntityType, LRelatedEntitySpecifier);

						if (LRelatedEntitySpecifier.Reference.Kind == ReferenceKind.Lookup)
						{
							GetReferenceLookup(AEntityQuery, LRelatedEntitySpecifier, LEntity);
						}
						else
						{
							GetReferenceChild(AEntityQuery, LRelatedEntitySpecifier, LEntity);
						}
					}
				}
				finally
				{
					LEntity.EndPopulate();
				}
			}
		}

		private void GetReferenceChild(EntityQuery AEntityQuery, RelatedEntitySpecifier ARelatedEntitySpecifier, Entity AEntity)
		{
			var LQuery = 
				from LEntityParameter in AEntityQuery.Filter
					where !(LEntityParameter.EntityName == ARelatedEntitySpecifier.EntitySpecifier.EntityTypeName
						&& ARelatedEntitySpecifier.RelatedColumnNames.Contains(LEntityParameter.Name))
					select LEntityParameter;

			var LParameters = LQuery.ToList();
			for (var i = 0; i < ARelatedEntitySpecifier.RelatedColumnNames.Length; i++)
				LParameters.Add(new EntityParameter(ARelatedEntitySpecifier.EntitySpecifier.EntityTypeName, ARelatedEntitySpecifier.RelatedColumnNames[i], AEntity[ARelatedEntitySpecifier.ColumnNames[i]].Value));

			foreach
			(
				var LChildEntity in
					Get
					(
						new EntityQuery
						(
							ARelatedEntitySpecifier.EntitySpecifier,
							LParameters.ToArray(),
							AEntityQuery.AsOf
						)
					)
			)
			{
				AEntity.Children[ARelatedEntitySpecifier.Reference.IndexName].Add(LChildEntity.ID, LChildEntity);
			}
		}

		private void GetReferenceLookup(EntityQuery AEntityQuery, RelatedEntitySpecifier ARelatedEntitySpecifier, Entity AEntity)
		{
			var LSourceId = new EntityID(AEntity, new EntityKey("sourceKey", ARelatedEntitySpecifier.ColumnNames));
			if (!LSourceId.IsNull)
			{
				var LQuery = 
					from LEntityParameter in AEntityQuery.Filter
						where LEntityParameter.EntityName != ARelatedEntitySpecifier.EntitySpecifier.EntityTypeName
							&& !ARelatedEntitySpecifier.RelatedColumnNames.Contains(LEntityParameter.Name)
						select LEntityParameter;

				var LParameters = LQuery.ToList();
				for (var i = 0; i < ARelatedEntitySpecifier.RelatedColumnNames.Length; i++)
					LParameters.Add(new EntityParameter(ARelatedEntitySpecifier.EntitySpecifier.EntityTypeName, ARelatedEntitySpecifier.RelatedColumnNames[i], LSourceId[i]));

				AEntity.Lookups[ARelatedEntitySpecifier.Reference.IndexName] =
					Get
					(
						new EntityQuery
						(
							ARelatedEntitySpecifier.EntitySpecifier,
							LParameters.ToArray(),
							AEntity.IsTemporal && !AEntityQuery.AsOf.Equals(DateTime.MinValue) ? AEntityQuery.AsOf : DateTime.Today
						)
					).Get(new EntityID(ARelatedEntitySpecifier.RelatedColumnNames, LSourceId));
			}
		}

		protected Entity LoadFromReader(ServerEntityType AServerEntityType, EntityType AEntityType, IDataReader AReader)
		{
			var LEntity = new Entity(AEntityType.Name);
			LEntity.BeginPopulate();
			try
			{
				EntityColumn LColumn;
				for (var LIndex = 0; LIndex < LEntity.Columns.Count; LIndex++)
				{
					LColumn = LEntity.Columns[LIndex];
					if (LColumn.ColumnType.IsStored)
						LColumn.Value = AReader[LColumn.Name];
				}

				AServerEntityType.DoAfterSelect(this, LEntity);
			}
			finally
			{
				LEntity.EndPopulate();
			}

			return LEntity;
		}

		protected EntitySet LoadSetFromReader(ServerEntityType AServerEntityType, EntityType AEntityType, IDataReader AReader)
		{
			var LEntitySet = new EntitySet(AEntityType.Name);

			while (AReader.Read())
			{
				if (AEntityType.PrimaryKey != null)
				{
					var LEntity = LoadFromReader(AServerEntityType, AEntityType, AReader);
					LEntitySet.Add(LEntity.ID, LEntity);
				}
				else
					LEntitySet.Add(LoadFromReader(AServerEntityType, AEntityType, AReader));
			}

			return LEntitySet;
		}

		/// <summary>
		/// Retrieves a set of entities.
		/// </summary>
		/// <param name="AEntityTypeName">The main type of the entities to retrieve.</param>
		/// <param name="AEntityCommandName">The name of the command to use for retrieval.</param>
		/// <param name="AFilter">A list of <see cref="EntityParameter"/> instances defining a filter for the retrieval.</param>
		/// <param name="AAsOf">The effective date for the retrieval.</param>
		/// <param name="AForUpdate">True if the entities should be retrieved for update.</param>
		/// <param name="ATraversal">An entity traversal describing the related entities to be retrieved with each entity.</param>
		/// <returns>An entity set containing the entities satisfying the request.</returns>
		public EntitySet Get(string AEntityTypeName, string AEntityCommandName, EntityParameter[] AFilter, DateTime AAsOf, bool AForUpdate, EntityTraversal ATraversal)
		{
			return
				Get
				(
					new EntityQuery
					(
						ATraversal == null 
							? new EntitySpecifier(AEntityTypeName, AEntityCommandName, AForUpdate) 
							: ResolveTraversal(AEntityTypeName, AEntityCommandName, AForUpdate, ATraversal),
						AFilter,
						AAsOf
					)
				);
		}

		/// <summary>
		/// Retrieves a set of entities.
		/// </summary>
		/// <param name="AEntityTypeName">The main type of the entities to retrieve.</param>
		/// <param name="AEntityCommandName">The name of the command to use for retrieval.</param>
		/// <param name="AFilter">A list of <see cref="EntityParameter"/> instances defining a filter for the retrieval.</param>
		/// <param name="AAsOf">The effective date for the retrieval.</param>
		/// <param name="ATraversal">An entity traversal describing the related entities to be retrieved with each entity.</param>
		/// <returns>An entity set containing the entities satisfying the request.</returns>
		public EntitySet Get(string AEntityTypeName, string AEntityCommandName, EntityParameter[] AFilter, DateTime AAsOf, EntityTraversal ATraversal)
		{
			return Get(AEntityTypeName, AEntityCommandName, AFilter, AAsOf, false, ATraversal);
		}
		
		/// <summary>
		/// Retrieves a set of entities.
		/// </summary>
		/// <param name="AEntityTypeName">The main type of the entities to retrieve.</param>
		/// <param name="AFilter">A list of <see cref="EntityParameter"/> instances defining a filter for the retrieval.</param>
		/// <param name="AAsOf">The effective date for the retrieval.</param>
		/// <param name="AForUpdate">True if the entities should be retrieved for update.</param>
		/// <param name="ATraversal">An entity traversal describing the related entities to be retrieved with each entity.</param>
		/// <returns>An entity set containing the entities satisfying the request.</returns>
		public EntitySet Get(string AEntityTypeName, EntityParameter[] AFilter, DateTime AAsOf, bool AForUpdate, EntityTraversal ATraversal)
		{
			return Get(AEntityTypeName, null, AFilter, AAsOf, AForUpdate, ATraversal);
		}

		/// <summary>
		/// Retrieves a set of entities.
		/// </summary>
		/// <param name="AEntityTypeName">The main type of the entities to retrieve.</param>
		/// <param name="AFilter">A list of <see cref="EntityParameter"/> instances defining a filter for the retrieval.</param>
		/// <param name="AAsOf">The effective date for the retrieval.</param>
		/// <param name="ATraversal">An entity traversal describing the related entities to be retrieved with each entity.</param>
		/// <returns>An entity set containing the entities satisfying the request.</returns>
		public EntitySet Get(string AEntityTypeName, EntityParameter[] AFilter, DateTime AAsOf, EntityTraversal ATraversal)
		{
			return Get(AEntityTypeName, null, AFilter, AAsOf, false, ATraversal);
		}

		/// <summary>
		/// Retrieves a set of entities.
		/// </summary>
		/// <param name="AEntityTypeName">The main type of the entities to retrieve.</param>
		/// <param name="AFilter">A list of <see cref="EntityParameter"/> instances defining a filter for the retrieval.</param>
		/// <param name="AAsOf">The effective date for the retrieval.</param>
		/// <param name="AForUpdate">True if the entities should be retrieved for update.</param>
		/// <returns>An entity set containing the entities satisfying the request.</returns>
		public EntitySet Get(string AEntityTypeName, EntityParameter[] AFilter, DateTime AAsOf, bool AForUpdate)
		{
			return Get(AEntityTypeName, AFilter, AAsOf, AForUpdate, null);
		}

		/// <summary>
		/// Retrieves a set of entities.
		/// </summary>
		/// <param name="AEntityTypeName">The main type of the entities to retrieve.</param>
		/// <param name="AFilter">A list of <see cref="EntityParameter"/> instances defining a filter for the retrieval.</param>
		/// <param name="AAsOf">The effective date for the retrieval.</param>
		/// <returns>An entity set containing the entities satisfying the request.</returns>
		public EntitySet Get(string AEntityTypeName, EntityParameter[] AFilter, DateTime AAsOf)
		{
			return Get(AEntityTypeName, AFilter, AAsOf, false, null);
		}

		/// <summary>
		/// Retrieves a set of entities.
		/// </summary>
		/// <param name="AEntityTypeName">The main type of the entities to retrieve.</param>
		/// <param name="AFilter">A list of <see cref="EntityParameter"/> instances defining a filter for the retrieval.</param>
		/// <param name="AForUpdate">True if the entities should be retrieved for update.</param>
		/// <param name="ATraversal">An entity traversal describing the related entities to be retrieved with each entity.</param>
		/// <returns>An entity set containing the entities satisfying the request.</returns>
		public EntitySet Get(string AEntityTypeName, EntityParameter[] AFilter, bool AForUpdate, EntityTraversal ATraversal)
		{
			return Get(AEntityTypeName, AFilter, DateTime.Today, AForUpdate, ATraversal);
		}

		/// <summary>
		/// Retrieves a set of entities.
		/// </summary>
		/// <param name="AEntityTypeName">The main type of the entities to retrieve.</param>
		/// <param name="AFilter">A list of <see cref="EntityParameter"/> instances defining a filter for the retrieval.</param>
		/// <param name="ATraversal">An entity traversal describing the related entities to be retrieved with each entity.</param>
		/// <returns>An entity set containing the entities satisfying the request.</returns>
		public EntitySet Get(string AEntityTypeName, EntityParameter[] AFilter, EntityTraversal ATraversal)
		{
			return Get(AEntityTypeName, AFilter, DateTime.Today, false, ATraversal);
		}

		/// <summary>
		/// Retrieves a set of entities.
		/// </summary>
		/// <param name="AEntityTypeName">The main type of the entities to retrieve.</param>
		/// <param name="AFilter">A list of <see cref="EntityParameter"/> instances defining a filter for the retrieval.</param>
		/// <param name="AForUpdate">True if the entities should be retrieved for update.</param>
		/// <returns>An entity set containing the entities satisfying the request.</returns>
		public EntitySet Get(string AEntityTypeName, EntityParameter[] AFilter, bool AForUpdate)
		{
			return Get(AEntityTypeName, AFilter, DateTime.Today, AForUpdate, null);
		}

		/// <summary>
		/// Retrieves a set of entities.
		/// </summary>
		/// <param name="AEntityTypeName">The main type of the entities to retrieve.</param>
		/// <param name="AFilter">A list of <see cref="EntityParameter"/> instances defining a filter for the retrieval.</param>
		/// <returns>An entity set containing the entities satisfying the request.</returns>
		public EntitySet Get(string AEntityTypeName, EntityParameter[] AFilter)
		{
			return Get(AEntityTypeName, AFilter, DateTime.Today, false, null);
		}

		/// <summary>
		/// Retrieves a set of entities.
		/// </summary>
		/// <param name="AEntityTypeName">The main type of the entities to retrieve.</param>
		/// <param name="AAsOf">The effective date for the retrieval.</param>
		/// <param name="AForUpdate">True if the entities should be retrieved for update.</param>
		/// <param name="ATraversal">An entity traversal describing the related entities to be retrieved with each entity.</param>
		/// <returns>An entity set containing the entities satisfying the request.</returns>
		public EntitySet Get(string AEntityTypeName, DateTime AAsOf, bool AForUpdate, EntityTraversal ATraversal)
		{
			return Get(AEntityTypeName, new EntityParameter[] { }, AAsOf, AForUpdate, ATraversal);
		}

		/// <summary>
		/// Retrieves a set of entities.
		/// </summary>
		/// <param name="AEntityTypeName">The main type of the entities to retrieve.</param>
		/// <param name="AAsOf">The effective date for the retrieval.</param>
		/// <param name="ATraversal">An entity traversal describing the related entities to be retrieved with each entity.</param>
		/// <returns>An entity set containing the entities satisfying the request.</returns>
		public EntitySet Get(string AEntityTypeName, DateTime AAsOf, EntityTraversal ATraversal)
		{
			return Get(AEntityTypeName, new EntityParameter[] { }, AAsOf, ATraversal);
		}

		/// <summary>
		/// Retrieves a set of entities.
		/// </summary>
		/// <param name="AEntityTypeName">The main type of the entities to retrieve.</param>
		/// <param name="AAsOf">The effective date for the retrieval.</param>
		/// <param name="AForUpdate">True if the entities should be retrieved for update.</param>
		/// <returns>An entity set containing the entities satisfying the request.</returns>
		public EntitySet Get(string AEntityTypeName, DateTime AAsOf, bool AForUpdate)
		{
			return Get(AEntityTypeName, new EntityParameter[] { }, AAsOf, AForUpdate, null);
		}

		/// <summary>
		/// Retrieves a set of entities.
		/// </summary>
		/// <param name="AEntityTypeName">The main type of the entities to retrieve.</param>
		/// <param name="AAsOf">The effective date for the retrieval.</param>
		/// <returns>An entity set containing the entities satisfying the request.</returns>
		public EntitySet Get(string AEntityTypeName, DateTime AAsOf)
		{
			return Get(AEntityTypeName, new EntityParameter[] { }, AAsOf, false, null);
		}

		/// <summary>
		/// Retrieves a set of entities.
		/// </summary>
		/// <param name="AEntityTypeName">The main type of the entities to retrieve.</param>
		/// <param name="AForUpdate">True if the entities should be retrieved for update.</param>
		/// <param name="ATraversal">An entity traversal describing the related entities to be retrieved with each entity.</param>
		/// <returns>An entity set containing the entities satisfying the request.</returns>
		public EntitySet Get(string AEntityTypeName, bool AForUpdate, EntityTraversal ATraversal)
		{
			return Get(AEntityTypeName, new EntityParameter[] { }, DateTime.Today, AForUpdate, ATraversal);
		}

		/// <summary>
		/// Retrieves a set of entities.
		/// </summary>
		/// <param name="AEntityTypeName">The main type of the entities to retrieve.</param>
		/// <param name="ATraversal">An entity traversal describing the related entities to be retrieved with each entity.</param>
		/// <returns>An entity set containing the entities satisfying the request.</returns>
		public EntitySet Get(string AEntityTypeName, EntityTraversal ATraversal)
		{
			return Get(AEntityTypeName, new EntityParameter[] { }, DateTime.Today, false, ATraversal);
		}

		/// <summary>
		/// Retrieves a set of entities.
		/// </summary>
		/// <param name="AEntityTypeName">The main type of the entities to retrieve.</param>
		/// <param name="AForUpdate">True if the entities should be retrieved for update.</param>
		/// <returns>An entity set containing the entities satisfying the request.</returns>
		public EntitySet Get(string AEntityTypeName, bool AForUpdate)
		{
			return Get(AEntityTypeName, new EntityParameter[] { }, DateTime.Today, AForUpdate, null);
		}

		/// <summary>
		/// Retrieves a set of entities.
		/// </summary>
		/// <param name="AEntityTypeName">The main type of the entities to retrieve.</param>
		/// <returns>An entity set containing the entities satisfying the request.</returns>
		public EntitySet Get(string AEntityTypeName)
		{
			return Get(AEntityTypeName, new EntityParameter[] { }, DateTime.Today, false, null);
		}

		/// <summary>
		/// Retreives an entity by its ID.
		/// </summary>
		/// <param name="AEntityTypeName">The type of entity to retrieve.</param>
		/// <param name="AId">The ID of the entity to retrieve.</param>
		/// <param name="AAsOf">The effective date for the retrieval.</param>
		/// <param name="AForUpdate">True if the entity should be retrieved for update.</param>
		/// <param name="ATraversal">A traversal describing the related entities to be retrieved with the entity.</param>
		/// <returns>An entity if one with the given ID exists, null otherwise.</returns>
		public Entity GetByID(string AEntityTypeName, EntityID AId, DateTime AAsOf, bool AForUpdate, EntityTraversal ATraversal)
		{
			return Get(AEntityTypeName, AId.ToEntityParameterArray(AEntityTypeName), AAsOf, AForUpdate, ATraversal).Get(AId);
		}

		/// <summary>
		/// Retreives an entity by its ID.
		/// </summary>
		/// <param name="AEntityTypeName">The type of entity to retrieve.</param>
		/// <param name="AId">The ID of the entity to retrieve.</param>
		/// <param name="AAsOf">The effective date for the retrieval.</param>
		/// <param name="ATraversal">A traversal describing the related entities to be retrieved with the entity.</param>
		/// <returns>An entity if one with the given ID exists, null otherwise.</returns>
		public Entity GetByID(string AEntityTypeName, EntityID AId, DateTime AAsOf, EntityTraversal ATraversal)
		{
			return Get(AEntityTypeName, AId.ToEntityParameterArray(AEntityTypeName), AAsOf, false, ATraversal).Get(AId);
		}

		/// <summary>
		/// Retreives an entity by its ID.
		/// </summary>
		/// <param name="AEntityTypeName">The type of entity to retrieve.</param>
		/// <param name="AId">The ID of the entity to retrieve.</param>
		/// <param name="AAsOf">The effective date for the retrieval.</param>
		/// <param name="AForUpdate">True if the entity should be retrieved for update.</param>
		/// <returns>An entity if one with the given ID exists, null otherwise.</returns>
		public Entity GetByID(string AEntityTypeName, EntityID AId, DateTime AAsOf, bool AForUpdate)
		{
			return Get(AEntityTypeName, AId.ToEntityParameterArray(AEntityTypeName), AAsOf, AForUpdate, null).Get(AId);
		}

		/// <summary>
		/// Retreives an entity by its ID.
		/// </summary>
		/// <param name="AEntityTypeName">The type of entity to retrieve.</param>
		/// <param name="AId">The ID of the entity to retrieve.</param>
		/// <param name="AAsOf">The effective date for the retrieval.</param>
		/// <returns>An entity if one with the given ID exists, null otherwise.</returns>
		public Entity GetByID(string AEntityTypeName, EntityID AId, DateTime AAsOf)
		{
			return Get(AEntityTypeName, AId.ToEntityParameterArray(AEntityTypeName), AAsOf, false, null).Get(AId);
		}

		/// <summary>
		/// Retreives an entity by its ID.
		/// </summary>
		/// <param name="AEntityTypeName">The type of entity to retrieve.</param>
		/// <param name="AId">The ID of the entity to retrieve.</param>
		/// <param name="AForUpdate">True if the entity should be retrieved for update.</param>
		/// <param name="ATraversal">A traversal describing the related entities to be retrieved with the entity.</param>
		/// <returns>An entity if one with the given ID exists, null otherwise.</returns>
		public Entity GetByID(string AEntityTypeName, EntityID AId, bool AForUpdate, EntityTraversal ATraversal)
		{
			return Get(AEntityTypeName, AId.ToEntityParameterArray(AEntityTypeName), DateTime.Today, AForUpdate, ATraversal).Get(AId);
		}

		/// <summary>
		/// Retreives an entity by its ID.
		/// </summary>
		/// <param name="AEntityTypeName">The type of entity to retrieve.</param>
		/// <param name="AId">The ID of the entity to retrieve.</param>
		/// <param name="ATraversal">A traversal describing the related entities to be retrieved with the entity.</param>
		/// <returns>An entity if one with the given ID exists, null otherwise.</returns>
		public Entity GetByID(string AEntityTypeName, EntityID AId, EntityTraversal ATraversal)
		{
			return Get(AEntityTypeName, AId.ToEntityParameterArray(AEntityTypeName), DateTime.Today, false, ATraversal).Get(AId);
		}

		/// <summary>
		/// Retreives an entity by its ID.
		/// </summary>
		/// <param name="AEntityTypeName">The type of entity to retrieve.</param>
		/// <param name="AId">The ID of the entity to retrieve.</param>
		/// <param name="AForUpdate">True if the entity should be retrieved for update.</param>
		/// <returns>An entity if one with the given ID exists, null otherwise.</returns>
		public Entity GetByID(string AEntityTypeName, EntityID AId, bool AForUpdate)
		{
			return Get(AEntityTypeName, AId.ToEntityParameterArray(AEntityTypeName), DateTime.Today, AForUpdate, null).Get(AId);
		}

		/// <summary>
		/// Retreives an entity by its ID.
		/// </summary>
		/// <param name="AEntityTypeName">The type of entity to retrieve.</param>
		/// <param name="AId">The ID of the entity to retrieve.</param>
		/// <returns>An entity if one with the given ID exists, null otherwise.</returns>
		public Entity GetByID(string AEntityTypeName, EntityID AId)
		{
			return Get(AEntityTypeName, AId.ToEntityParameterArray(AEntityTypeName), DateTime.Today, false, null).Get(AId);
		}

		protected virtual int ExecuteCommand(ServerEntityCommand ACommand, params Param[] AInputParams)
		{
			return
				Database.Execute
				(
					ACommand.CommandType == ServerEntityCommandType.Statement
						? CommandType.Text 
						: CommandType.StoredProcedure,
					ACommand.CommandText,
					AInputParams
				);
		}

		protected virtual void Insert(Entity AEntity)
		{
			var LServerEntityType = EntityTypeProvider.GetServerEntityType(AEntity.EntityTypeName);

			var LInputParams = new List<Param>();

            LServerEntityType.DoBeforeInsert(this, AEntity);

			Param LIdentityParam = null;

			foreach (var LColumn in AEntity.Columns)
			{
				if (!LColumn.ColumnType.IsStored)
					continue;

				if (LColumn.ColumnType.Name == LServerEntityType.IdentityColumnName)
				{
					LIdentityParam = new Param("@" + LColumn.Name, ParameterDirection.Output, LColumn.NativeType);
					LInputParams.Add(LIdentityParam);
				}
				else
					LInputParams.Add(new Param("@" + LColumn.Name, LColumn.NativeType, LColumn.Value));
			}
			
			ExecuteCommand(LServerEntityType.InsertCommand, LInputParams.ToArray());

			if (LServerEntityType.HasIdentityColumn)
			{
				AEntity.BeginPopulate();
				try
				{
					AEntity[LServerEntityType.IdentityColumnName].Value = LIdentityParam.Value;
				}
				finally
				{
					AEntity.EndPopulate();
				}
			}

			LServerEntityType.DoAfterInsert(this, AEntity);
		}

		protected virtual void Update(Entity AEntity)
		{
			var LServerEntityType = EntityTypeProvider.GetServerEntityType(AEntity.EntityTypeName);
			var LEntityType = AEntity.EntityType;

			var LInputParams = new List<Param>();

			foreach (var LColumn in AEntity.Columns)
			{
				if (LColumn.ColumnType.IsComputed)
					continue;

				LInputParams.Add(new Param("@" + LColumn.Name, LColumn.Value));
				LInputParams.Add(new Param("@Old_" + LColumn.Name, LColumn.OriginalValue));
			}
			
			var LRowsAffectedParam = new Param("@RowsAffected", ParameterDirection.Output, typeof(int));
			LInputParams.Add(LRowsAffectedParam);
			
            LServerEntityType.DoBeforeUpdate(this, AEntity);

			ExecuteCommand(LServerEntityType.UpdateCommand, LInputParams.ToArray());
			
			if (Convert.ToInt32(LRowsAffectedParam.Value) == 0)
				throw new ConcurrencyException(String.Format("{0} could not be updated because it has been changed by another user since it was read.", LEntityType.Title));

			LServerEntityType.DoAfterUpdate(this, AEntity);
		}

		protected virtual void Delete(Entity AEntity)
		{
			var LServerEntityType = EntityTypeProvider.GetServerEntityType(AEntity.EntityTypeName);
			var LEntityType = AEntity.EntityType;

			var LInputParams = new List<Param>();

			foreach (var LColumn in AEntity.Columns)
			{
				if (LColumn.ColumnType.IsComputed)
					continue;

				LInputParams.Add(new Param("@" + LColumn.Name, LColumn.Value));
				LInputParams.Add(new Param("@Old_" + LColumn.Name, LColumn.OriginalValue));
			}
			
			var LRowsAffectedParam = new Param("@RowsAffected", ParameterDirection.Output, typeof(int));
			LInputParams.Add(LRowsAffectedParam);
			
			LServerEntityType.DoBeforeDelete(this, AEntity);

			ExecuteCommand(LServerEntityType.DeleteCommand, LInputParams.ToArray());

			if (Convert.ToInt32(LRowsAffectedParam.Value) == 0)
				throw new ConcurrencyException(String.Format("{0} cannot be deleted because it has been changed by another user since it was read.", LEntityType.Title));

			LServerEntityType.DoAfterDelete(this, AEntity);
		}

		protected virtual void SaveEntity(Entity AEntity, bool AIsChild)
		{
			if (AEntity.IsNew)
			{
				if (!AIsChild || AEntity.IsRelevant)
					Insert(AEntity);
			}
			else if (AEntity.IsDeleted)
			{
				Delete(AEntity);
			}
			else if (AEntity.IsModified)
			{
				if (!AIsChild || AEntity.IsRelevant)
					Update(AEntity);
				else
				{
					AEntity.MarkToDelete(AEntity.FromDate);
					Delete(AEntity);
				}
			}
		}

		protected virtual void InternalSave(Entity AEntity, bool AIsChild)
		{
			SaveEntity(AEntity, AIsChild);

			foreach (var LChildren in AEntity.Children)
				foreach (var LChildEntity in LChildren)
					InternalSave(LChildEntity, true);
		}

		/// <summary>
		/// Saves any outstanding changes to the entity.
		/// </summary>
		/// <param name="AEntity">The entity to be saved.</param>
		public virtual void Save(ref Entity AEntity)
		{
			var LClonedEntity = AEntity.Clone();
			try
			{
				InternalSave(AEntity, false);
				AEntity.AcceptAllChanges();
			}
			catch
			{
				AEntity = LClonedEntity;
				throw;
			}
		}
	}
}
